# Sketch - A Python-based interactive drawing program
# Copyright (C) 1998, 1999, 2000 by Bernhard Herzog
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.	See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307	USA

from Sketch import const
CHANGED = const.CHANGED
from Sketch.connector import Publisher
from Sketch import CreateListUndo, UndoAfter, NullUndo, SketchInternalError, _
from Sketch.warn import pdebug, INTERNAL

from pattern import SolidPattern, EmptyPattern
from color import StandardColors
from blend import Blend


class Style(Publisher):

    is_dynamic = 0
    name = ''

    def __init__(self, name = '', duplicate = None, **kw):
        if duplicate is not None:
            self.__dict__ = duplicate.__dict__.copy()
            if hasattr(self, 'fill_pattern'):
                self.fill_pattern = self.fill_pattern.Copy()
            if hasattr(self, 'line_pattern'):
                self.line_pattern = self.line_pattern.Copy()
        else:
            if name:
                self.name = name
            for key, value in kw.items():
                setattr(self, key, value)

    def SetProperty(self, prop, value):
        dict = self.__dict__
        if dict.has_key(prop):
            undo = (self.SetProperty, prop, dict[prop])
        else:
            undo = (self.DelProperty, prop)
        if prop == 'fill_pattern' or prop == 'line_pattern':
            value = value.Copy()
        dict[prop] = value
        self.issue(CHANGED, self)
        return undo

    def DelProperty(self, prop):
        undo = (self.SetProperty, prop, getattr(self, prop))
        delattr(self, prop)
        self.issue(CHANGED, self)
        return undo

    def Duplicate(self):
        if self.is_dynamic:
            return self
        return self.__class__(duplicate = self)

    def Copy(self):
        return self.__class__(duplicate = self)

    def Name(self):
        return self.name

    def SetName(self, name):
        undo = self.SetName, self.name
        self.name = name
        return undo

    def AsDynamicStyle(self):
        result = self.Copy()
        result.is_dynamic = 1
        return result

    def AsUndynamicStyle(self):
        result = self.Copy()
        if self.is_dynamic:
            del result.is_dynamic
            del result.name
        return result

    def SaveToFile(self, file):
        if self.is_dynamic:
            file.DynamicStyle(self)

    def IsEmpty(self):
        return not self.__dict__

def FillStyle(pattern):
    return Style(fill_pattern = pattern)

EmptyFillStyle = Style(fill_pattern = EmptyPattern)

def LineStyle(color = None, width = 0, cap  = const.CapButt,
              join  = const.JoinMiter, dashes = None,
              arrow1 = None, arrow2 = None):
    return Style(line_pattern = SolidPattern(color), line_width = width,
                 line_cap = cap, line_join = join, line_dashes = dashes,
                 line_arrow1 = arrow1, line_arrow2 = arrow2)

SolidLine = LineStyle
EmptyLineStyle = Style(line_pattern = EmptyPattern)

class PropertyStack:

    update_cache = 1

    def __init__(self, base = None, duplicate = None):
        if duplicate is not None:
            self.stack = []
            for layer in duplicate.stack:
                self.stack.append(layer.Duplicate())
        else:
            if base is None:
                base = factory_defaults.Duplicate()
            self.stack = [base]

    def __getattr__(self, attr):
        if self.update_cache:
            cache = self.__dict__
            stack = self.stack[:]
            stack.reverse()
            for layer in stack:
                cache.update(layer.__dict__)
            self.update_cache = 0
        try:
            return self.__dict__[attr]
        except KeyError:
            raise AttributeError, attr

    def _clear_cache(self):
        self.__dict__ = {'stack' : self.stack}
        return (self._clear_cache,)

    def prop_layer(self, prop):
        # return property layer containing PROP
        for item in self.stack:
            if hasattr(item, prop):
                return item
        # we should never reach this...
        raise SketchInternalError('unknown graphics property "%s"' % prop)

    def set_property(self, prop, value):
        layer = self.prop_layer(prop)
        if layer.is_dynamic:
            if self.stack[0].is_dynamic:
                layer = Style()
                setattr(layer, prop, value)
                return self.add_layer(layer)
            else:
                layer = self.stack[0]
        return layer.SetProperty(prop, value)

    def SetProperty(self, **kw):
        stack = self.stack
        undo = []
        append = undo.append
        if len(stack) == 1 and not stack[0].is_dynamic:
            set = stack[0].SetProperty
            for prop, value in kw.items():
                append(set(prop, value))
        else:
            set = self.set_property
            for prop, value in kw.items():
                append(set(prop, value))

        if len(self.stack) > 1:
            undo_stack = (self.set_stack, self.stack[:])
            if self.delete_shadowed_layers():
                undo.append(undo_stack)
        undo = CreateListUndo(undo)
        undo = (UndoAfter, undo, self._clear_cache())
        return undo

    def set_stack(self, stack):
        undo = (self.set_stack, self.stack)
        self.stack = stack
        self._clear_cache()
        return undo

    def delete_shadowed_layers(self):
        # check if some styles are completely hidden now
        stack = self.stack
        layers = []
        dict = {'name':1, 'is_dynamic':0}
        dict.update(stack[0].__dict__)
        length = len(dict)
        for layer in stack[1:]:
            dict.update(layer.__dict__)
            if length != len(dict):
                layers.append(layer)
            length = len(dict)
        length = len(stack)
        stack[1:] = layers
        return length != len(stack)

    def add_layer(self, layer):
        undo = (self.set_stack, self.stack[:])
        self.stack.insert(0, layer)
        return undo
    load_AddStyle = add_layer

    def AddStyle(self, style):
        if style.is_dynamic:
            undo = self.add_layer(style)
            self.delete_shadowed_layers()
            self._clear_cache()
            return undo
        else:
            return apply(self.SetProperty, (), style.__dict__)


    def HasFill(self):
        return self.fill_pattern is not EmptyPattern

    def IsAlgorithmicFill(self):
        return self.fill_pattern.is_procedural

    def ExecuteFill(self, device, rect = None):
        self.fill_pattern.Execute(device, rect)

    def HasLine(self):
        return self.line_pattern is not EmptyPattern

    def IsAlgorithmicLine(self):
        return self.line_pattern.is_procedural

    def ExecuteLine(self, device, rect = None):
        line_pattern = self.line_pattern
        if line_pattern is not EmptyPattern:
            line_pattern.Execute(device, rect)
            device.SetLineAttributes(self.line_width, self.line_cap,
                                     self.line_join, self.line_dashes)

    def HasFont(self):
        return self.font is not None

    def ObjectChanged(self, object):
        if object in self.stack:
            self._clear_cache()
            return 1
        return 0

    def ObjectRemoved(self, object):
        if object in self.stack:
            idx = self.stack.index(object)
            undo = (self.set_stack, self.stack[:])
            self.stack[idx] = self.stack[idx].AsUndynamicStyle()
            pdebug('properties', 'made style undynamic')
            return undo
        return NullUndo

    def Untie(self):
        info = []
        for i in range(len(self.stack)):
            style = self.stack[i]
            if style.is_dynamic:
                self.stack[i] = style.AsUndynamicStyle()
                info.append((i, style))
        self._clear_cache()
        return info

    def Tie(self, document, info):
        for i, style in info:
            s = document.GetDynamicStyle(style.Name())
            if s == style:
                self.stack[i] = s
        self._clear_cache()

    def Duplicate(self):
        return self.__class__(duplicate = self)


    grow_join = [5.240843064, 0.5, 0.5]
    grow_cap = [None, 0.5, 0.5, 0.70710678]

    def GrowAmount(self):
        return self.line_width * max(self.grow_cap[self.line_cap],
                                     self.grow_join[self.line_join])

    def Blend(self, other, frac1, frac2):
        result = {}
        for prop, func in blend_functions:
            if func:
                result[prop] = func(getattr(self, prop), getattr(other, prop),
                                    frac1, frac2)
            else:
                result[prop] = getattr(self, prop)
        return PropertyStack(apply(Style, (), result))

    def Transform(self, trafo, rects):
        # XXX hardcoding which properties may need to be transformed is
        # not really a good idea, but it's significantly faster.
        undo = NullUndo
        if len(self.stack) == 1 and not self.stack[0].is_dynamic:
            if self.fill_transform:
                pattern = self.fill_pattern
                if pattern.is_procedural:
                    undo = pattern.Transform(trafo, rects)
        elif self.fill_transform:
            pattern = self.fill_pattern
            if pattern.is_procedural:
                pattern = pattern.Duplicate()
                if pattern.Transform(trafo, rects) is not NullUndo:
                    undo = self.set_property('fill_pattern', pattern)
        if undo is not NullUndo:
            undo = (UndoAfter, undo, self._clear_cache())
        return undo

    def CreateStyle(self, which_properties):
        properties = {}
        for prop in which_properties:
            if property_types[prop] == FontProperty and not self.HasFont():
                continue
            properties[prop] = getattr(self, prop)
        return apply(Style, (), properties)

    def DynamicStyleNames(self):
        names = []
        for style in self.stack:
            if style.is_dynamic:
                names.append(style.Name())
        return names

    def condense(self):
        stack = self.stack
        last = stack[0]
        for style in stack[1:]:
            if not last.is_dynamic and not style.is_dynamic:
                dict = style.__dict__.copy()
                dict.update(last.__dict__)
                last.__dict__ = dict
            last = style
        length = len(stack)
        self.delete_shadowed_layers()

    def SaveToFile(self, file):
        file.Properties(self)


class _EmptyProperties:
    def HasFill(self):
        return 0
    HasLine = HasFill
    HasFont = HasFill

    def DynamicStyleNames(self):
        return []

EmptyProperties = _EmptyProperties()

#
#
#

factory_defaults = Style()
default_graphics_style = None # set below
default_text_style = None # set below
blend_functions = []
property_names = []
property_titles = {}
property_types = {}
transform_properties = []

def blend_number(n1, n2, frac1, frac2):
    return n1 * frac1 + n2 * frac2

LineProperty = 1
FillProperty = 2
FontProperty = 3
OtherProperty = -1

def _set_defaults(prop, title, short_title, type, value,
                  blend = None, transform = 0):
    factory_defaults.SetProperty(prop, value)
    property_names.append(prop)
    property_titles[prop] = (title, short_title)
    property_types[prop] = type
    blend_functions.append((prop, blend))
    if transform:
        transform_properties.append(prop)

black = StandardColors.black

# XXX the default properties should be defined by the user.
_set_defaults('fill_pattern', _("Fill Pattern"), _("Pattern"), FillProperty,
              EmptyPattern, blend = Blend, transform = 1)
_set_defaults('fill_transform', _("Fill Transform Pattern"),
              _("Transform pattern"), FillProperty, 1)
_set_defaults('line_pattern', _("Line Pattern"), _("Pattern"), LineProperty,
              SolidPattern(black), blend = Blend, transform = 1)
_set_defaults('line_width', _("Line Width"), _("Width"), LineProperty, 1 ,
              blend = blend_number)
_set_defaults('line_cap', _("Line Cap"), _("Cap"), LineProperty,
              const.CapButt)
_set_defaults('line_join', _("Line Join"), _("Join"), LineProperty,
              const.JoinMiter)
_set_defaults('line_dashes', _("Line Dashes"), _("Dashes"), LineProperty, ())
_set_defaults('line_arrow1', _("Line Arrow 1"), _("Arrow 1"), LineProperty,
              None)
_set_defaults('line_arrow2', _("Line Arrow 2"), _("Arrow 2"), LineProperty,
              None)
_set_defaults('font', _("Font"), _("Font"), FontProperty, None)
_set_defaults('font_size', _("Font Size"), _("Size"), FontProperty, 12,
              blend = blend_number)

factory_text_style = factory_defaults.Copy()
factory_text_style.fill_pattern = SolidPattern(black)
factory_text_style.line_pattern = EmptyPattern
factory_text_style.font = None
default_graphics_style = factory_defaults.Copy()
default_text_style = factory_defaults.Copy()
default_text_style.fill_pattern = SolidPattern(black)
default_text_style.line_pattern = EmptyPattern
default_text_style.font = None


def FactoryTextStyle():
    import Sketch
    style = factory_text_style.Copy()
    if style.font is None:
        fontname = Sketch.config.preferences.default_font
        style.font = Sketch.GetFont(fontname)
    return style

def DefaultTextProperties():
    import Sketch
    if default_text_style.font is None:
        fontname = Sketch.config.preferences.default_font
        default_text_style.font = Sketch.GetFont(fontname)
    return PropertyStack(base = default_text_style.Copy())

def DefaultGraphicsProperties():
    return PropertyStack(base = default_graphics_style.Copy())

def set_graphics_defaults(kw):
    for key, value in kw.items():
        if not property_types[key] == FontProperty:
            if key in ('fill_pattern', 'line_pattern'):
                value = value.Copy()
            default_graphics_style.SetProperty(key, value)

def set_text_defaults(kw):
    for key, value in kw.items():
        if not property_types[key] == LineProperty:
            if key == 'fill_pattern':
                value = value.Copy()
            default_text_style.SetProperty(key, value)

